閱讀本篇文章前,仔細想想看
- 大概可以解釋普通 JS 物件(也就是 JSON 格式,或筆者所謂的狹義物件)在 TypeScript 裡的推論機制。
- 知道筆者表達的廣義跟狹義物件的差別在哪裡嗎~?(儘管這不是寫程式圈子裡的正統詞彙,但卻是筆者用來好說明本系列文章而衍生出來的詞)
- 為何我們不常直接對變數做
object
型別的註記?如果還沒理解完畢的話,可以先翻看前一篇文章喔!
[2019.09.15 新增] tsconfig.json 設定
這裡筆者必須緊急說明:若讀者試著筆者舉的程式碼範例的話,請讀者記得將裡面的
strictNullCheck
選項改成true
,這一點忘記在文章系列的一開頭提醒讀者,實在是很抱歉!/* tsconfig.json */ { "compilerOptions": { /* ... */ "strictNullChecks": true, /* ... */ } }
因此請讀者注意,目前學習的 TypeScript 型別系統版本多了一個
strictNullCheck
的編譯器屬性設定!至於為何會造成如此狀況,那是因為筆者在專案上習慣將某些 TypeScript 編譯器設定啟動!至於strictNullCheck
到底為何,將會在型別系統講述告一段落後,開始講述 TypeScript 的編譯器設定檔喔![2019.09.18 新增] 程式碼範例
如果想要看到本系列文裡面舉的程式碼範例可以參考 Maxwell-Alexius/Iron-Man-Competition 這個 GitHub Repo 喔~寫作過程當中會不斷更新的!
在進入正文之前,我們再把前一篇得知的重要定律給重新看過一次:
前一篇推演出的結論:廣義物件完整性定律
廣義物件在被 TypeScript 推論的狀態下,屬性不能被任意新增或更改成其他型別。能夠做的事情只有:
- 全面覆寫,廣義物件的屬性對照型別格式也要完全對位
- 更改廣義物件本身就擁有屬性對應的值,其中:要帶入的值的型態必須對應到該屬性的型態
我們稱這樣的行為為「保持廣義物件的完整性」。
稍微有印象後,那麼我們就 ...
正文開始!
一樣沿用前一天的環境,因此如果有任何問題可以參考前一篇!
今天可以講得稍微輕鬆一些了,來看看函式型別的推論與註記吧!先從最基本的開始。(結果如圖一)
// index.ts
let aSimpleFunction = function() { console.log('Hi!'); };
圖一:函式物件的推論結果
嗯~我想大部分的讀者應該對這種格式並不陌生:() => void
,這個函式格式 —— 它的輸入端是空的,輸出端是 void
代表的也是空值,或者代表不回傳的狀態。
再來一個簡單範例,比如:
const addition = function (num1, num2) {
return num1 + num2;
};
結果呢,我們被 TS 警告了(圖二為顯示錯誤的點,圖三為錯誤訊息),這邊的點就很重要了!(哪裡的點?請不要亂想!)
圖二:TS 明顯對於宣告過後的函數參數(Parameters)很有意見
圖三:原來,TS 還是會介意參數,只是我們看到神秘的錯誤訊息(其實說神秘理解過後一點也不神秘)
any
TypeScript 對於函式參數(Parameters)的推論結果是 —— any
!
不過讀者可能想說,如果是 any
的話,TypeScript 不是不管的嗎?
這裡我們就要反推一下,如果 TypeScript 完全對函式參數不予理會的話,首先第一個想得到的問題是:
它到底要如何推論出函式輸出的型別呢?通常是先知道輸入的型別是什麼,才能間接推論輸出的型別吧!
第二個問題 —— 典型的蛋生雞、雞生蛋的概念 —— 站在 TypeScript 的立場想想看:
如果你是 TS,你真的能夠推論函式應該要放入的型別是什麼嗎?
那麼假設今天開發者想要讓 addition
函式不是用在數字上,而是字串上的連接。但光是看到:
const addition = function (param1, param2) {
return param1 + param2;
};
TS 根本無從推論說你到底是要讓函式的輸入作為什麼型別,因此得到最後的結論 —— 把參數一率推論為 any
,不過 TypeScript 照樣會提醒:“你是不是忘記對參數註記型別?如果是這樣我只能把這個函式的參數當成 any
囉?”
如果 TS 真的把函式的參數當成 any
,那讀者可真的就得小心了。
在這裡我們強制將參數註記為 any
,以下的程式碼連 TypeScript 也不會想鳥你的。(結果如圖四)
圖四:筆者明知故犯的錯誤,TypeScript 也沒有發出任何警訊,但這是錯誤的行為
因此筆者很感謝 TypeScript 留意的這一類潛在 Bug 出現的狀況:在 TypeScript 的世界裡,我們稱這個現象為 Implicit any
(隱性的 any
推論,中文真的很難翻,我們之後還是用 Implicit Any 來代表我們遇到的這種問題吧!)
我們重新描述一下這個問題以及對 Implicit Any 下一個定義:
重點 1. Implicit Any
大部分的情況下,只要定義任何函式,TypeScript 通常會無條件推論函式內的參數(Parameters)為
any
型別,這種現象我們稱之為 Implicit Any。
讀者試試看
請讀者試著回想一下,過往有沒有在使用 JavaScript 開發的過程,犯過類似的錯誤?
這裡舉筆者遇過的例子,我們通常會把
addition
這個函式當成是對數字做相加。但筆者踩過的雷是:有時候數字的來源 —— 比如呼叫後端 Server 出來的 JSON Response,明明內容是數字,但解析出來的結果是字串類型(例如:字串的"42"
而非數字的42
)。在這種情況下筆者沒注意到,結果要除錯就真的麻煩了,數字的字串加數字的字串還是等於長得很像數字的字串,但是是那種沒數字計算意義上的行為。
let number1 = addition('3', '4'); // => '34' let number2 = addition(3, 4); // => 7
哇,這在除錯上更為難人,要是 TypeScript 能夠提前警告筆者定義的
addition
函式裡的參數只能接收數字型態的值,根本就不用再走這除 Bug 冤枉路!
TypeScript 在大部分的狀況下會搞不清楚函式裡參數的型別是什麼,因此會對我們發出 Implicit any
的警訊。不過筆者還是強調一下這幾個字:
“在大部分的情況下”
那是什麼情況下,TS 認得函式裡參數的型別呢?這裡筆者就先埋這個梗(很快就會在後續的篇章有答案!所以我們要看下去~),讓讀者想想看,站在 TS 的角度下,你會在什麼情況下可以直接篤定地推論出某函式的參數它的型別呢?
可能有些讀者會覺得麻煩:“我今天是來看教學文,我不想傷我的腦”。其實筆者也可以選擇直接講出答案,但是這樣就缺少了自由思考的空間。如果讀者大概想過,再回來讀文章,相信會把印象烙得更深刻喔!
回顧一下(筆者的廢話可以跳過)
講到這裡,筆者光是把型別推論(Type Inference)從原始型別、狹義物件到現在的函式物件分析過後,是不是覺得比想像中複雜呢?
筆者事實上也這麼覺得,因此花了很多時間為了一個 Inference Behaviour in TypeScript 整理到寫文章中途也有點亂掉的狀態。
但是基礎不打好的話,就不能篤定說:“我們是真正理解 TypeScript 型別系統的人”。看來要成為型別系統達人的話,不是隨隨便便地跟別人說:“有這些工具可以用”,結果推論機制的細節以及到底要在哪裡註記也講不出來。
這樣實在是不行,這對筆者來說實在是太膚淺(相信對讀者來說也很膚淺 XD),既然要學好一門東西還是得把全局掌握著會比較好。
從剛剛筆者呈現的案例得知被定義出來的函式到底有多脆弱,很容易就被開發者誤用!
在沒有認清或協議好函式到底要怎麼用的狀況(型別只能接受什麼?怎麼正確使用?Document 有沒有寫好?真的沒有例外嗎?我可以亂填東西嗎?),只會造成更多亂七八糟的補丁狀況,比如:把各種型別狀況用一個個判斷敘述給處理掉,或直觀一點 —— 丟出例外 throw new Error
等等。
函式的參數註記(Parameter Type Annotation)其實很簡單。想要使剛剛的 addition
函數的參數只能接受數字型別的話,這樣做就可以了:
神奇的事情在後頭,如果再把剛剛錯誤的範例更正,變成這樣(結果如圖五,錯誤訊息在圖六):
圖五:正確的函式參數註記方法
圖六:函式的輸出可以藉由輸入的型別來推論
TypeScript 在我們還沒註記函式的輸出部分(沒錯,也可以註記函式輸出的型別喔!)就已經幫我們把輸出的型別藉由輸入參數的型別推論出來了,因此我們可以很安心地使用這個函式。
在此我們可以下另外一個結論:
重點 2. 函式的推論與註記
分別為輸入參數與輸出部分,大部分情況下,只要我們提供函式參數的註記,輸出就可以間接被 TypeScript 推論出來
那麼哪些是就算我們有了參數註記卻也不能得知輸出型別的狀態呢?
探討這個問題前,我們先來看看輸出部分到底如何註記(Return Type Annotation)。其實還是蠻簡單的:
好的,那讀者會問:“真的有函式可以設計到回傳的型別是不確定或未知的嗎?”。
有的!有些情況設計出來的函式,其回傳的型別只能是 any
~
有些人可能就繼續問:“誒!可是你不是說我們必須儘量避免 any
嗎?”
這個就是筆者形容極少數狀況下會使用 any
的其中一種 Case:請看看 JSON.parse
這個函式(確切來說是方法,畢竟方法跟函式還是有差別)到底被 TS 推論出什麼?(圖七)
圖七:哇,好長一串,但重點在於它的輸出型別是 any
仔細看一下這段:
(method) JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any
我們整理一下:
讀者看到有個 reviver?
對應的是另一種看起來是函式型別的東西,而且該型別還加 |
代表 union
。這個部分會在後續講到 Optional Properties / Parameters(選用屬性或參數,中文有點難聽,英文比較適合)以及明文型別(Literal Type)會再多多說明喔~
(以下就不稱 JSON.parse
為函式,不然會誤導一些讀者。至於不知道函式跟方法的差別的讀者,請自行上網查詢,因為不在本系列重點)
這裡的重點是:我們看到了這個方法回傳的型別是 any
,其實用過 JSON.parse
應該也會覺得合情合理,畢竟 JSON 格式就有無限多種,除了用 object
作為回傳格式外,應該也只能用 any
來表示。
這時候就要跟讀者說,遇到這種要把 any
型別的值帶入某變數 —— 可以加上型別註記來讓 TypeScript 發揮作用(這裡的型別註記也是用明文型別做範例,因此等我們講到明文型別會再補充),你會發現以下所有的狀況 TS 都接受(結果如圖八):
圖八:三種告訴 TS 編譯器,我們的變數應該要是什麼型別或長什麼樣子
如果你不這麼做,就會違反在討論原始型別篇章時一開始講的:“儘量不要讓變數被推論為 any
狀態”(如圖九)
圖九:畢竟你不跟 TS 說,TS 也只能將函數回傳的 any
型別套到變數身上
但如果把型別註記加上去,情況就不同了。(如圖十)
圖十:加上了型別註記,就等同於讓 TypeScript 幫我們關注型別喔,撿回使用 TS 的根本優勢
重點 3. 函式回傳
any
型別遇到函式是回傳
any
型別的值,我們必須主動對該值作型別註記(Type Annotation),找回開發 TypeScript 的優勢 —— 也就是 TS 提供的型別系統(Type System)
如果讀者真的很清楚筆者整理的“廣義物件完整性定律”的話 —— 我們可以完全覆寫函式型別,只要格式正確,TS 就很安全地給過!(範例如圖十一的,圖十二和十三分別為兩種案例的錯誤情形)
圖十一:驗證廣義物件完整性的定律,結論是格式一但錯誤就不能被覆寫
圖十二:參數型別錯了,因此被 TS 提醒
圖十三:就連忘記回傳值也會被提醒喔
另一種情形是 —— 根據廣義物件完整性定律,我們也可以覆寫函數物件的屬性,但是應該沒人想過要這樣做吧...
// 想試試看覆寫掉 Function.prototype.bind 之類的方法嗎?
addition.bind = function ...
筆者在這裡認為,覆寫掉函式的屬性(或方法,畢竟方法也算是一種屬性,只是該屬性對應的值是一個函式)—— 除非讀者有特別需求,否則筆者實在是想不透為何要這麼做,因此這裡就不提供覆寫函式物件屬性的驗證囉。
void
回想剛剛其中的範例,我們注意到:
儘管有些讀者認為 —— 函式不回傳值就是 void
型別 —— 這點被視為理所當然的,不過筆者還是整理並且強調一下:
重點 4. 函式不回傳值的型態
若定義的函式不回傳值的話,不管有沒有被註記,型別推論結果會被認定為
void
讀者試試看
筆者這邊讓讀者想想看並主動實驗一下,以下的範例哪些會被 TS 認為錯誤?如果 TS 通過的話,那型別推論結果會是什麼?
今天總算把函式物件的型別在 TypeScript 裡面被推論和註記的機制都了解完畢囉~ 但別忘記了,我們還有陣列這東西要搞定,敬請期待下一篇的分析~
hello,請教下
let strJson = '{"name":"周星馳","age":23}';
// let strJsonObj = <{ name: number; age: string }>JSON.parse(strJson);
那麼就不符合他的輸出型別啦~ts也不會報錯,那怎麼可好?
parse
出來是any
所以不會報錯。
這邊是因為示範,所以我們自己預設一筆已知型別的資料。
真實情況則是:
我們接到的資料非常有可能不會符合我們要的型別格式,
通常會再視需求重塑,因此在接到資料時還會再檢驗或轉型處理
以上是我的看法,歡迎交流及指正~
如果讀者真的很清楚筆者整理的“廣義物件完整性定律”的話 —— 我們可以完全覆寫函式型別,只要格式正確,TS 就很安全地給過!
這邊所謂函式的格式,應該是指 function signature 吧!一般翻為函式簽署;另外:
let addition = function(param1: number, param2: number) {
return param1 + param2;
};
addition = function(param1: number, param2: number) {
return param2 + param1;
};
並沒有覆寫 addition
的型別,只是重新指定了另一個函式值,addition
的型別依舊是 function(number, number): number
。
PS. VSCode 會把 addition
的型別顯示為 function(param1: number, param2: number): number
,不過那是不對的,參數名稱一般不被視為函式簽署的一部份,事實上 TypeScript 也不這麼認為。